package llama.view.panels;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.Serializable;
import java.text.Collator;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import javax.swing.DefaultListModel;
import javax.swing.DropMode;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.TransferHandler;
import javax.swing.border.EtchedBorder;

import llama.core.Contact;
import llama.core.ContactList;
import llama.core.IUser;
import llama.ctrl.AssignTagsCtrl;
import llama.ctrl.ControllerFactory;
import llama.ctrl.ConversationCtrl;
import llama.ctrl.ConversationCtrl.ConversationOption;

/**
 * This class is responsible for showing an list of Contacts which can be interacted with.  
 */
public class ContactListPanel extends JPanel {
	private ContactList contactList;
	private IUser activeUser;
	
	private static final int CELL_HEIGHT = 20;
	
	private JList jList;
	private DefaultListModel listModel;
	private JScrollPane scrollPane;
	private TreeMap<String,Boolean> storedTagMap;
	private JLabel lblEmail = new JLabel(" ");
	
	
	/**
	 * Create the panel.
	 */
	public ContactListPanel() {
		setLayout(new BorderLayout(0, 0));
		
		scrollPane = new JScrollPane();
		scrollPane.setBorder(null);
		scrollPane.setViewportBorder(new EtchedBorder(EtchedBorder.LOWERED, null, null));
		add(scrollPane);
		
		listModel = new DefaultListModel();
		
		jList = new JList();
		jList.setModel(listModel);
		jList.setBorder(null);
		
		/* For drag and drop rearrange */
		jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // For now
		jList.setDragEnabled(true);
		jList.setDropMode(DropMode.INSERT);
		jList.setTransferHandler(new ContactDropHandler());
		new ContactDragListener();
		
		jList.setCellRenderer(new ContactListCellRenderer());
		jList.setVisibleRowCount(-1);
		jList.setLayoutOrientation(JList.VERTICAL);
		jList.setFixedCellHeight(CELL_HEIGHT);
		jList.addKeyListener(new ListKeyListener());
		jList.addMouseListener(new ListMouseListener());
		
		scrollPane.setViewportView(jList);
		scrollPane.setColumnHeaderView(lblEmail);
	}
	
	/**
	 * This methods shows the Contacts in the provided ContactList.<p>
	 * When showing the Contacts they will be sorted into tag-sections which can be collapsed and expanded.
	 * The tags will be extracted from the ContactList and if there's a locally stored tag-Map it will affect collapsing/expanding.
	 * @see clearStoredTagMap
	 * @param contactList the list of Contacts which should be shown.
	 */
	public void processContacts(final ContactList contactList) {
		if(contactList == null) {
			throw new IllegalArgumentException("processContacts: contactList is not allowed to be null");
		}
		
		//Store a local copy of contactList - Used to expand/collapse
		//(Eliminates the need to Request ContactList when just expanding/collapsing)
		this.contactList = contactList;

		//Make a working copy of contact list and then sort
		ContactList copyOfContactList = (ContactList) contactList.clone();
		Collections.sort(copyOfContactList, new CompareContact());
		
		//Update stored tagMap - possibly merge or store newTagMap
		updateStoredTagMap(copyOfContactList);
		
		//Prepare JList-data - Iterate over tags and pop contacts
		removeCollapsedContacts(copyOfContactList);
		
		//Get and set List-data
		Object[] obj = getListData(copyOfContactList);
		setListData(obj);
		
		jList.revalidate();
		jList.repaint();
	}
	
	/**
	 * Clears the stored tag-Map.
	 */
	public void clearStoredTagMap() {
		storedTagMap = null;
	}
	
	/**
	 * Adds all provided objects to jList.
	 * @param obj the object(s) that should be added.
	 */
	private void setListData(Object[] obj) {
		listModel.clear();
		for (Object object : obj) {
			listModel.addElement(object);
		}
	}
	
	/**
	 * Update the stored tag-Map with tags from the provided contact-list.<br>
	 * A tag-Map will be extracted from the contact-list (collapsing defaults to false).<br>
	 * If there's no stored tag-Map the extracted map will be stored.<br>
	 * If there's a stored tag-Map, the stored tag-Map will be used to update the the extracted map afterwards the latter is stored.  
	 * @param contactList with the tags that should be stored.
	 */
	private void updateStoredTagMap(ContactList contactList) {
		if(storedTagMap == null) {
			storedTagMap = getTagMapFromContacts(contactList);
		} else {
			TreeMap<String,Boolean> contactListTagMap = getTagMapFromContacts(contactList);
			mergeWithStoredTagMap(contactListTagMap);
		}
	}
	
	/**
	 * The provided tag-Map's values are updated with the stored tag-Map's.<br>
	 * Tags (keys) which only exists stored will not be transferred to the new tag-Map. The new tag-Map will be stored, 
	 * replacing the old.  
	 * @param newTagMap the tag-map that should be updated and stored.
	 */
	private void mergeWithStoredTagMap(Map<String,Boolean> newTagMap) {
		Set<Map.Entry<String, Boolean>> set = storedTagMap.entrySet();
			
		for (Entry<String,Boolean> entry : set) {
			if (newTagMap.containsKey(entry.getKey())) {
				newTagMap.put(entry.getKey(), entry.getValue());
			}
		}
		storedTagMap = new TreeMap<String, Boolean>(newTagMap);
	}
	
	/**
	 * Extracts tags from the Contacts in the provided lists and returns them as a TreeMap.<br>
	 * Collapsing defaults to false.
	 * @param contactList the ContactList from which tags will be extracted.
	 * @return A TreeMap containing the extracted tags.
	 */
	private TreeMap<String,Boolean> getTagMapFromContacts(ContactList contactList) {
		TreeMap<String,Boolean> contactListTagMap = new TreeMap<String,Boolean>();
		for (Contact contact : contactList) {
			contactListTagMap.put(contact.getTag(), false);
		}
		return contactListTagMap;
	}
	
	/**
	 * This method returns an Object array of ContactCells and SeperatorCells.<p>
	 * The SeperatorCells are created with tags from the stored tag-Map and ContactCells are created 
	 * with Contacts from the provided ContactList. <br>
	 * The SeperatorCells will be sorted alphabetically and the ContactCells will be distributed to 
	 * their tag-sections and sorted alphabetically within these sections.
	 * @param contactList - the list of Contacts which should be sorted into tag-sections.
	 * @return an Object array of ContactCells and SeperatorCells.
	 */
	private Object[] getListData(ContactList contactList) {
		Object[] listData = new Object[storedTagMap.size()+contactList.size()]; //Amount of tags + contacts
		
		//Create Iterator for the stored tag-Map
		Set<Map.Entry<String, Boolean>> set = storedTagMap.entrySet();
		Iterator<Entry<String, Boolean>> tagIterator = set.iterator();
		
		int i = 0;
		while(tagIterator.hasNext()) {
			//Get tag-info and create SeperatorCell 
			Map.Entry<String,Boolean> me = tagIterator.next();
			String currentTag = me.getKey();
			boolean collapsed = me.getValue();
			listData[i] = new SeperatorCell(currentTag, collapsed);
			
			int j = 1; //Contains steps stepped (1+contacts created)
			ListIterator<Contact> contactIterator = contactList.listIterator();
			while(contactIterator.hasNext()) {
				Contact tempContact = contactIterator.next();
				if(currentTag.equals(tempContact.getTag())) {
					listData[i+j] = new ContactCell(tempContact);
					contactIterator.remove(); //remove to improve speed of next outer-loop.
					j++;
				}
					
			}	
			i+=j;
		}
		
		return listData;
	}
	
	/**
	 * Removes the Contacts of the provided lists which has a tag marked to be collapsed in the stored tag-Map. 
	 * @param contactList - the list of Contacts which should have collapses pop'd.
	 */
	private void removeCollapsedContacts(ContactList contactList) {
		Set<Map.Entry<String, Boolean>> set = storedTagMap.entrySet();
		Iterator<Entry<String, Boolean>> tagIterator = set.iterator();
		while(tagIterator.hasNext()) {
			Map.Entry<String,Boolean> me = tagIterator.next();
			String currentTag = me.getKey();
			boolean collapsed = me.getValue();
			
			if(collapsed) {
				ListIterator<Contact> contactIterator = contactList.listIterator();
				while(contactIterator.hasNext()) {
					Contact tempContact = contactIterator.next();
					if(currentTag.equals(tempContact.getTag())) {
						contactIterator.remove();
					}
						
				}	
			}
		}
	}
	
	/**
	 * The list has been double-clicked.<br>
	 * If a ContactCell has been double-clicked - select a conversation with the Contact.
	 */
	private void listDoubledClicked() {
		if((jList.getSelectedValue() != null) && (jList.getSelectedValue() instanceof ContactCell)) {
			ContactCell contactCell = (ContactCell)jList.getSelectedValue();
			ConversationCtrl conversationCtrl = (ConversationCtrl) ControllerFactory.getController(ConversationCtrl.class);
			conversationCtrl.setSelectedContact(ConversationOption.SELECT, contactCell.getContact().getUser());
			conversationCtrl.execute();
		}
	}
	
	/**
	 * The list has been single-clicked.<br>
	 * If a SeperatorCell has been single-clicked - toggle collapse of this tag-section.
	 */
	private void listSingleClicked() {
		if((jList.getSelectedValue() != null) && (jList.getSelectedValue() instanceof SeperatorCell)) {
			SeperatorCell sc = (SeperatorCell)jList.getSelectedValue();
			String tag = sc.getTag();
			toggleTagMapValue(tag);
			processContacts(contactList);
		}
	}
	
	/**
	 * Toggles the boolean-value of the entry with the provided String-key. 
	 * @param tag - the key for the entry in the stored tag-Map which should be updated.
	 */
	private void toggleTagMapValue(String tag) {
		boolean toggle = storedTagMap.get(tag) ? false : true;
		storedTagMap.put(tag, toggle);
	}
	
	
	/**
	 * This class is responsible for taking a Contact and displaying it in a List. 
	 */
	@SuppressWarnings("serial")
	private class ContactCell extends JPanel {
		private JLabel lblName;
		private Contact contact;
		
		public ContactCell(Contact contact) {
			this.contact = contact;

			setLayout(new BorderLayout());
			lblName = new JLabel(contact.getUser().getLastName()+", "+contact.getUser().getFirstName());
			add(lblName);
		}
		
		public Contact getContact() {
			return contact;//.clone();
		}
		
		public void setIcon(ImageIcon icon){
			lblName.setIcon(icon);
		}
	}
	
	/**
	 * This class is responsible for taking a tag and displaying it in a List. 
	 */
	@SuppressWarnings("serial")
	private class SeperatorCell extends JPanel {
		private JLabel lblName;
		public SeperatorCell(String tag, boolean collapsed) {
			setLayout(new BorderLayout());
			lblName = new JLabel(tag);
			if(collapsed) {
				lblName.setIcon(new ImageIcon("img/plus.png"));
			} else {
				lblName.setIcon(new ImageIcon("img/minus.png"));
			}
			add(lblName);
		}
		/**
		 * Get the tag displayed.
		 * @return the tag in display.
		 */
		public String getTag() {
			return lblName.getText();
		}
	}
	
	/**
	 * This class compares Contacts.
	 */
	private static class CompareContact implements Comparator<Contact>, Serializable {
		/**
		 * Compares in an alphabetical fashion (Collator.TERTIARY) by full name.<p>
		 * A full name is the last name + the first name of the IUser 
		 * stored in the Contact. 
		 */
		@Override
		public int compare(Contact o1, Contact o2) {
			Collator col = Collator.getInstance();
			col.setStrength(Collator.TERTIARY);
			IUser user1 = o1.getUser();
			IUser user2 = o2.getUser();
			
			return col.compare(user1.getLastName()+user1.getFirstName(), 
					user2.getLastName()+user2.getLastName());
		}
		
	}
	
	private class ListKeyListener extends KeyAdapter {
		@Override
		public void keyPressed(java.awt.event.KeyEvent e) {
			if(e.getKeyCode() == KeyEvent.VK_ENTER && jList.getSelectedIndex() != -1) {
				listDoubledClicked();
			}
		}
	}
	
	private class ListMouseListener extends MouseAdapter {
		@Override
		public void mousePressed(MouseEvent e) {
			if (e.isPopupTrigger()) {
				showMenu(e);
			}
		}
		
		@Override
		public void mouseReleased(MouseEvent e) {
			if (e.isPopupTrigger()) {
				showMenu(e);
			}
		}
		
		@Override
		public void mouseClicked(java.awt.event.MouseEvent e) {
			if(e.getClickCount()==2) {
				listDoubledClicked();
			} else {
				listSingleClicked();
			}
		}
		
		/**
		 * Shows a context menu for the Contact selected in the jList.
		 * @param e the MouseEvent generated.
		 */
		private void showMenu(MouseEvent e) {
			//Select the item
			jList.setSelectedIndex(jList.locationToIndex(e.getPoint())); 
			
			//Check to see if a contact is selected in the list
			if((jList.getSelectedValue() != null) && (jList.getSelectedValue() instanceof ContactCell)) {
				Contact contact = ((ContactCell) jList.getSelectedValue()).getContact();

				//Show the menu
				ContactMenu contactMenu = new ContactMenu(contact);
				contactMenu.show(jList, e.getX(), e.getY()); 
			}
		}
	}
	
	private static class ContactListCellRenderer implements ListCellRenderer {

		@Override
		public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
			if((value != null) && (value instanceof ContactCell)) {
				ContactCell  contactCell = (ContactCell) value;
				if(isSelected) {
					contactCell.setBackground(new Color(9,205,218));
				} else {
					contactCell.setBackground(Color.white);
				}
				
//				System.out.println("CELL RENDER: " + contactCell.getContact().getUser().getFirstName() + " " + contactCell.getContact().getUser().getStatus());
				if(contactCell.getContact().getUser().isOnline()){
					contactCell.setIcon(new ImageIcon("img/online.gif"));
				}
				else {
					contactCell.setIcon(new ImageIcon("img/offline.gif"));
				}
				
				return contactCell;
			} else if((value != null) && (value instanceof SeperatorCell)) {
				return (SeperatorCell)value;
				
			} else {
				return new JLabel("Error: Unknown object in ContactListCellRenderer."); //Shouldn't happen.
			}
		}
		
	}
	
	private class ContactDragListener implements DragSourceListener, DragGestureListener {
		DragSource ds = new DragSource();
		
		public ContactDragListener() {
			DragGestureRecognizer dgr = ds.createDefaultDragGestureRecognizer(jList, DnDConstants.ACTION_MOVE, this);
		}

		@Override
		public void dragGestureRecognized(DragGestureEvent dge) {
			StringSelection transferable = new StringSelection(Integer.toString(jList.getSelectedIndex()));
			ds.startDrag(dge, DragSource.DefaultCopyDrop, transferable, this);
		}
		@Override
		public void dragDropEnd(DragSourceDropEvent dsde) {
		}
		@Override
		public void dragEnter(DragSourceDragEvent dsde) {
		}
		@Override
		public void dragExit(DragSourceEvent dse) {
		}
		@Override
		public void dragOver(DragSourceDragEvent dsde) {
		}
		@Override
		public void dropActionChanged(DragSourceDragEvent dsde) {
		}
	}
	
	private class ContactDropHandler extends TransferHandler {
		@Override
		public boolean canImport(TransferHandler.TransferSupport support) {
			if (!support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
				return false;
			}
			JList.DropLocation dl = (JList.DropLocation) support.getDropLocation();
			if (dl.getIndex() == -1) {
				return false;
			}
			else {
				return true;
			}
		}
		
		@Override
		public boolean importData(TransferHandler.TransferSupport support) {
			if (!canImport(support)) {
				return false;
			}
			
			Transferable transferable = support.getTransferable();
			String indexString;
			try {
				indexString = (String) transferable.getTransferData(DataFlavor.stringFlavor);
			} catch (Exception e) {
				return false;
			}
			
			int index = Integer.parseInt(indexString);
			JList.DropLocation dl = (JList.DropLocation) support.getDropLocation();
			int dropTargetIndex = dl.getIndex();
			
			/* Contact successfully moved */

			ListModel listModel = jList.getModel();
			AssignTagsCtrl assignTagsCtrl = (AssignTagsCtrl)ControllerFactory.getController(AssignTagsCtrl.class);
			if (jList.getSelectedValue() instanceof ContactCell) {
				ContactCell contactCell = (ContactCell)listModel.getElementAt(index);
				
				String tag = null;
				for (int i=0; i<dropTargetIndex; i++) {
					if (listModel.getElementAt(i) instanceof SeperatorCell) {
						SeperatorCell cell = (SeperatorCell)listModel.getElementAt(i);
						tag = cell.getTag();
					}
				}
				if (tag != null) {
					assignTagsCtrl.assignTag(contactCell.getContact().getUser(), tag);
					assignTagsCtrl.execute();
				}
			}
			
			return true;
		}
	}
	
	public void setActiveUser(IUser user) {
		this.activeUser = user;
		lblEmail.setText(this.activeUser.getEmail());
	}
}
